import { PendingPromise, pendingResolve } from "./pendingPromise";

const NEXT = Symbol();
const PREV = Symbol();
type LevelsNode = { [NEXT]?: LevelsMap; pendings?: PendingPromise<any>[]; [PREV]?: LevelsNode; key: string };
type LevelsMap = Map<any, LevelsNode>;
export class LevelsLock {
  public getPendings: (keys: any[]) => PendingPromise<any>[] | undefined;
  public addPendings: (keys: any[]) => PendingPromise<any>[];
  public delPendings: (keys: any[]) => void;
  constructor() {
    const map: LevelsMap = new Map();
    this.getPendings = (keys: any[]) => this.__getPendings(map, keys);
    this.addPendings = (keys: any[]) => this.__addPendings(map, keys);
    this.delPendings = (keys: any[]) => this.__delPendings(map, keys);
  }
  private findSyncNode(syncMap: LevelsMap, keys: any[], creation: false): [LevelsMap | undefined, LevelsNode | undefined, PendingPromise<any>[] | undefined];
  private findSyncNode(syncMap: LevelsMap, keys: any[], creation: true): [LevelsMap, LevelsNode, PendingPromise<any>[]];
  private findSyncNode(syncMap: LevelsMap, keys: any[], creation: boolean) {
    let prntNode: LevelsNode | undefined;
    let map: LevelsMap | undefined = syncMap;
    for (let i = 0; !!map && i < keys.length - 1; i++) {
      let lastNode = prntNode;
      prntNode = map.get(keys[i]);
      if (!prntNode) {
        if (creation) {
          prntNode = { [NEXT]: new Map(), [PREV]: lastNode, key: keys[i] };
          map.set(keys[i], prntNode);
          map = prntNode[NEXT];
        } else {
          map = undefined;
        }
      } else if (!(map = prntNode[NEXT])) {
        if (creation) {
          prntNode[NEXT] = new Map();
          map = prntNode[NEXT];
        } else {
          if (i < keys.length - 2) prntNode = undefined;
        }
      }
    }
    let mapNode = map?.get(keys[keys.length - 1]);
    if (creation) {
      if (!map) {
        new Error("Impossible Error: No Prev Map.");
      } else if (!mapNode) {
        map.set(keys[keys.length - 1], (mapNode = { pendings: [], [PREV]: prntNode, key: keys[keys.length - 1] }));
      } else if (!mapNode.pendings) {
        mapNode.pendings = [];
      }
    }
    return [map, mapNode, mapNode?.pendings];
  }
  private __getPendings(syncMap: LevelsMap, keys: any[]) {
    return this.findSyncNode(syncMap, keys, false)[2];
  }
  private __addPendings(syncMap: LevelsMap, keys: any[]) {
    return this.findSyncNode(syncMap, keys, true)[2];
  }
  private __delPendings(syncMap: LevelsMap, keys: any[]) {
    const [finalMap, finalVal] = this.findSyncNode(syncMap, keys, false);
    if (!!finalMap && !!finalVal) {
      // delete pending
      delete finalVal.pendings;
      // delete above including self
      this.__tryDeleteNodeAndAbove(syncMap, finalVal);
    }
  }
  private __tryDeleteNodeAndAbove(syncMap: LevelsMap, node?: LevelsNode) {
    while (!!node) {
      const nextMap = node[NEXT];
      if (!node.pendings && (!nextMap || nextMap.size === 0)) {
        const nodeKey = node.key;
        node = node[PREV];
        const map = node?.[NEXT] || syncMap;
        map.delete(nodeKey);
      } else {
        break;
      }
    }
  }
}
const asyncOneMap = new LevelsLock();
export const asyncOne = async <T>(call: () => Promise<T>, ...keys: any[]): Promise<T> => {
  let pendings = asyncOneMap.getPendings(keys);
  if (!!pendings) {
    return (pendings[pendings.length] = pendingResolve<T>());
  } else {
    pendings = asyncOneMap.addPendings(keys);
    try {
      const result = await call.call(null);
      pendings.forEach(p => setTimeout(() => p.resolve(result)));
      return result;
    } catch (e: unknown) {
      const ex = e instanceof Error ? e : new Error(JSON.stringify(e));
      pendings.forEach(p => setTimeout(() => p.reject(ex)));
      throw e;
    } finally {
      asyncOneMap.delPendings(keys);
    }
  }
};
const asyncWaitMap = new LevelsLock();
export const asyncWait = async <T>(call: () => Promise<T>, ...keys: any[]) => {
  let pendings = asyncWaitMap.getPendings(keys);
  /*sleep*/ if (!!pendings) await (pendings[pendings.length] = pendingResolve<void>());
  /*continue-------------*/ else pendings = asyncWaitMap.addPendings(keys);
  try {
    return await call.call(null);
  } finally {
    const next4awaken = pendings.shift();
    /*awaken the next*/ if (next4awaken !== undefined) next4awaken.resolve();
    /*unlock-----------------------------------*/ else asyncWaitMap.delPendings(keys);
  }
};
